package com.whz.iplocation.ipregion.search;

import com.whz.iplocation.ipregion.search.db.Region;
import com.whz.iplocation.utils.ToolUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 分析二进制文件byte[],生成可识别的{ip,位置}
 */
@Slf4j
public class ByteIPLocation {

    private volatile byte[] data;

    private volatile long firstIndexOffset;

    private volatile long totalIndexCount;

    private static final byte REDIRECT_MODE_1 = 0x01;

    private static final byte REDIRECT_MODE_2 = 0x02;

    private static final long IP_RECORD_LENGTH = 7;

    private static final ReentrantLock lock = new ReentrantLock();

    public static ByteIPLocation locate(final byte[] data) {
        return new ByteIPLocation(data);
    }

    private ByteIPLocation(final byte[] data) {
        this.data = data;
        firstIndexOffset = read4ByteAsLong(0);
        long lastIndexOffset = read4ByteAsLong(4);
        totalIndexCount = (lastIndexOffset - firstIndexOffset) / IP_RECORD_LENGTH + 1;
    }

    /**
     * 刷新ip数据
     *
     * @param data ip数据库
     */
    public void updateData(final byte[] data) {
        lock.lock();
        try {
            this.data = data;
            firstIndexOffset = read4ByteAsLong(0);
            long lastIndexOffset = read4ByteAsLong(4);
            totalIndexCount = (lastIndexOffset - firstIndexOffset) / IP_RECORD_LENGTH + 1;
        } finally {
            lock.unlock();
        }
    }

    private long read4ByteAsLong(final int offset) {
        long val = data[offset] & 0xFF;
        val |= (data[offset + 1] << 8L) & 0xFF00L;
        val |= (data[offset + 2] << 16L) & 0xFF0000L;
        val |= (data[offset + 3] << 24L) & 0xFF000000L;
        return val;
    }

    private long read3ByteAsLong(final int offset) {
        long val = data[offset] & 0xFF;
        val |= (data[offset + 1] << 8) & 0xFF00;
        val |= (data[offset + 2] << 16) & 0xFF0000;
        return val;
    }

    private long search(long ip) {
        long low = 0;
        long high = totalIndexCount;
        long mid;
        while (low <= high) {
            mid = (low + high) >>> 1;
            long indexIP = read4ByteAsLong((int) (firstIndexOffset + (mid - 1) * IP_RECORD_LENGTH));
            long indexIPNext = read4ByteAsLong((int) (firstIndexOffset + mid * IP_RECORD_LENGTH));
            if (indexIP <= ip && ip < indexIPNext) {
                return read3ByteAsLong((int) (firstIndexOffset + (mid - 1) * IP_RECORD_LENGTH + 4));
            } else {
                if (ip > indexIP) {
                    low = mid + 1;
                } else if (ip < indexIP) {
                    high = mid - 1;
                }
            }
        }
        return -1;
    }

    /**
     * 按照IP查找对应地址
     *
     * @param ip 目标IP
     * @return 对应地址
     */
    public Region getLocation(final String ip) {
        long numericIp = ToolUtils.ipToNum(ip);
        lock.lock();
        long offset = search(numericIp);
        try {
            if (offset != -1) {
                Region region = new Region();
                region.setIp(ip);
                readIPLocation((int) offset, region);
                log.info("Search Ip:'{}',location:{}", ip, region);
                return region;
            }
        } finally {
            lock.unlock();
        }
        log.info("Search Ip:'{}',location: unknown", ip);
        return null;
    }

    private void readIPLocation(final int offset, Region region) {
        try {
            String countryStr;
            String area;
            byte redirectMode = data[offset + 4];
            if (redirectMode == REDIRECT_MODE_1) {
                long countryOffset = read3ByteAsLong(offset + 5);
                redirectMode = data[(int) countryOffset];
                if (redirectMode == REDIRECT_MODE_2) {
                    final QQWryString country = readString((int) read3ByteAsLong((int) countryOffset + 1));
                    countryStr = country.string;
                    countryOffset = countryOffset + 4;
                } else {
                    final QQWryString country = readString((int) countryOffset);
                    countryStr = country.string;
                    countryOffset += country.byteCountWithEnd;
                }
                area = readArea((int) countryOffset);
            } else if (redirectMode == REDIRECT_MODE_2) {
                countryStr = readString((int) read3ByteAsLong(offset + 5)).string;
                area = readArea(offset + 8);
            } else {
                final QQWryString country = readString(offset + 4);
                countryStr = country.string;
                area = readArea(offset + 4 + country.byteCountWithEnd);
            }
            region.setCountry(toUtf8(countryStr));
            region.setArea(toUtf8(area));
        } catch (Exception e) {
            log.error("search IP location error : " + region.getIp(), e);
        }
    }

    private String toUtf8(String input) {
        if (StringUtils.isEmpty(input)) {
            return "";
        }
        return new String(input.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
    }

    private String readArea(final int offset) {
        byte redirectMode = data[offset];
        if (redirectMode == REDIRECT_MODE_1 || redirectMode == REDIRECT_MODE_2) {
            long areaOffset = read3ByteAsLong(offset + 1);
            if (areaOffset == 0) {
                return "";
            } else {
                return readString((int) areaOffset).string;
            }
        } else {
            return readString(offset).string;
        }
    }

    private QQWryString readString(int offset) {
        int pos = offset;
        final byte[] b = new byte[128];
        int i;
        for (i = 0, b[i] = data[pos++]; b[i] != 0; ) {
            b[++i] = data[pos++];
        }
        return new QQWryString(new String(b, 0, i, Charset.forName("GBK")), i + 1);
    }

    private static class QQWryString {

        public final String string;

        public final int byteCountWithEnd;

        public QQWryString(final String string, final int byteCountWithEnd) {
            this.string = string;
            this.byteCountWithEnd = byteCountWithEnd;
        }

        @Override
        public String toString() {
            return string;
        }

    }
}